// Copyright © 2022 Cisco Systems, Inc. and its affiliates.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package types

import (
	"encoding/json"
	"fmt"
	"sort"

	log "github.com/sirupsen/logrus"

	"github.com/openclarity/openclarity/core/to"
)

type Vulnerability struct {
	ID          string   `json:"id"`
	Description string   `json:"description,omitempty"`
	Links       []string `json:"links"`
	Distro      Distro   `json:"distro"`
	CVSS        []CVSS   `json:"cvss"`
	Fix         Fix      `json:"fix"`
	Severity    string   `json:"severity,omitempty"`
	Package     Package  `json:"package"`
	LayerID     string   `json:"layerID"`
	Path        string   `json:"path"`
}

func (v Vulnerability) sorted() Vulnerability {
	sort.Slice(v.CVSS, func(i, j int) bool {
		return v.CVSS[i].Version < v.CVSS[j].Version
	})
	sort.Strings(v.Links)
	sort.Strings(v.Fix.Versions)
	sort.Strings(v.Package.CPEs)
	sort.Strings(v.Package.Licenses)
	return v
}

type CVSS struct {
	Source         string      `json:"source,omitempty"`
	Type           string      `json:"type,omitempty"`
	Version        string      `json:"version"`
	Vector         string      `json:"vector"`
	Metrics        CvssMetrics `json:"metrics"`
	VendorMetadata interface{} `json:"vendorMetadata"`
}

type CvssMetrics struct {
	BaseScore           float64  `json:"baseScore"`
	ExploitabilityScore *float64 `json:"exploitabilityScore,omitempty"`
	ImpactScore         *float64 `json:"impactScore,omitempty"`
}

type Fix struct {
	Versions []string `json:"versions"`
	State    string   `json:"state"`
}

func (f Fix) enrich(ff Fix) Fix {
	f.Versions = to.UniqueSlice(append(f.Versions, ff.Versions...))

	if f.State == "" || f.State == "unknown" {
		f.State = ff.State
	}

	return f
}

type Package struct {
	Name     string   `json:"name"`
	Version  string   `json:"version"`
	Type     string   `json:"type"`     // apk, gem, deb, ...
	Language string   `json:"language"` // java, python, go, ...
	Licenses []string `json:"licenses"`
	CPEs     []string `json:"cpes"`
	PURL     string   `json:"purl"`
}

// Distro provides information about a detected Linux distribution.
type Distro struct {
	Name    string   `json:"name"`    // Name of the Linux distribution
	Version string   `json:"version"` // Version of the Linux distribution (major or major.minor version)
	IDLike  []string `json:"idLike"`  // the ID_LIKE field found within the /etc/os-release file
}

func (d Distro) enrich(dd Distro) Distro {
	if d.Name == "" {
		d.Name = dd.Name
	}

	if d.Version == "" {
		d.Version = dd.Version
	}

	d.IDLike = to.UniqueSlice(append(d.IDLike, dd.IDLike...))

	return d
}

type VulnerabilityKey string // Unique identification of a vulnerability ID per package (name and version)

func NewVulnerabilityKey(vulnerability Vulnerability) VulnerabilityKey {
	return VulnerabilityKey(fmt.Sprintf("%s.%s.%s", vulnerability.ID, vulnerability.Package.Name, vulnerability.Package.Version))
}

// handleVulnerabilityWithExistingKey merges two vulnerabilities with the same key.
func handleVulnerabilityWithExistingKey(vulnerability Vulnerability, otherVulnerability Vulnerability) Vulnerability {
	// Adopt longest description
	description := vulnerability.Description
	if len(otherVulnerability.Description) > len(description) {
		description = otherVulnerability.Description
	}

	// Merge CVSS
	cvss := to.UniqueSliceByKey(append(vulnerability.CVSS, otherVulnerability.CVSS...), func(cvss CVSS) string {
		b, err := json.Marshal(cvss)
		if err != nil {
			log.Errorf("failed to marshal cvss. c=%+v: %v", cvss, err)
			return "" // skip
		}

		return string(b)
	})

	return Vulnerability{
		ID:          vulnerability.ID, // Keep the original ID
		Description: description,
		Links:       to.UniqueSlice(append(vulnerability.Links, otherVulnerability.Links...)),
		Distro:      vulnerability.Distro.enrich(otherVulnerability.Distro),
		CVSS:        cvss,
		Fix:         vulnerability.Fix.enrich(otherVulnerability.Fix),
		Severity:    vulnerability.Severity, // Keep the original severity
		Package:     vulnerability.Package,  // Keep the original package
		LayerID:     vulnerability.LayerID,  // Keep the original layerID
		Path:        vulnerability.Path,     // Keep the original path
	}
}
